跳到主要内容

Go 的 HTTP 标准库-服务端-工作原理

http 标准库

无需框架,Go 语言本身就提供了 http 标准库,可以非常方便地搭建 HTTP 服务端和客户端

net/http 是 Go 语言中原生的 http 实现,可以提供 http 服务器的功能,其中默认的 DefaultServeMux 提供了基础的路有功能。 net/http 提供了良好的抽象:Server,Listener,Conn,HandlerFunc,Handler 定义了一整套 http 请求的处理流程。

http 库的执行流程

客户端发起的 HTTP 请求是通过 Go 语言实现的 HTTP 服务器监听、接收、处理并返回响应的,这个 HTTP 服务器底层工作流程如下:

  1. 创建 Listen Socket,监听指定的端口,等待客户端请求到来;
  2. Listen Socket 接收客户端的请求,得到 Client Socket,接下来通过 Client Socket 与客户端通信;
  3. 处理客户端的请求,首先从 Client Socket 读取 HTTP 请求的协议头, 如果是 POST 方法, 还可能要读取客户端提交的数据,然后交给相应的 Handler(处理器)处理请求,Handler 处理完毕后装载好客户端需要的数据,最后通过 Client Socket 返回给客户端。

创建 Listen Socket 监听端口

err := http.ListenAndServe(":8000", nil)

该方法底层调用的是 net/http 包的 ListenAndServe 方法,首先会初始化一个 Server 对象

然后调用该 Server 实例的 ListenAndServe 方法,进而调用 net.Listen("tcp", addr),也就是基于 TCP 协议创建 Listen Socket,并在传入的 IP 地址和端口号上监听请求,在本例中,IP 地址为空,默认是本机地址,端口号是 8000:

接收客户端请求并建立连接

创建 Listen Socket 成功后,调用 Server 实例的 Serve(net.Listener) 方法,用来接收并处理客户端的请求信息。

这个方法里面起了一个 for 循环,在循环体中首先通过 net.Listener(即上一步监听端口中创建的 Listen Socket)实例的 Accept 方法接收客户端请求,接收到请求后根据请求信息创建一个 conn 连接实例,最后单独开了一个 goroutine,把这个请求的数据当做参数扔给这个 conn 去服务:

// 上面那张图最后调用的就是这个函数
func (srv *Server) Serve(l net.Listener) error {
...

for {
rw, err := l.Accept()
if err != nil {
....
}
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}

这个就是高并发体现了,用户的每一次请求都是在一个新的 goroutine 去服务,相互不影响。客户端请求的具体处理逻辑都是在 c.serve 中完成的。

处理客户端请求并返回响应

err := http.ListenAndServe(":8000", nil)

接下来,可以进入 conn 实例的 serve 方法源码,看看底层如何将 HTTP 请求分配给指定处理器方法进行处理。(代码太长就不贴了)

总之 conn 首先会通过 c.readRequest() 解析请求,然后在 serverHandler{c.server}.ServeHTTP(w, w.req) 的 ServeHTTP 方法(如下代码)中获取相应的 handler := sh.srv.Handler,也就是在调用函数 ListenAndServe 时候的第二个参数。

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}

if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {
var allowQuerySemicolonsInUse int32
req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {
atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)
}))
defer func() {
if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {
sh.srv.logf("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192")
}
}()
}

handler.ServeHTTP(rw, req)
}

因为上面传的是 nil,则默认会获取 DefaultServeMux,这个 handler 变量其实就是一个路由器,它用来匹配 URL 路由与对应的处理函数,而这个映射关系在 main 函数的第一行代码中就完成了:

http.HandleFunc("/", sayHelloWorld)

其作用就是注册了请求 / 的路由规则,当请求 URL 路由为 /,就会跳转到函数 sayhelloWorld 来处理请求,DefaultServeMux 会调用 ServeHTTP 方法,这个方法内部其实就是调用 sayhelloWorld 方法本身

可以看这个 HandleFunc 方法源码

在 Go 语言中函数本身是第一类公民,可以当作实现了 Handler 接口的类型,只不过对应的的 ServeHTTP 方法内部调用的是函数自身而已

最后通过写入 ResponseWriter 对象将响应返回到客户端

DefaultServeMux 底层实现

上面说到

http.HandleFunc("/", sayHelloWorld)
err := http.ListenAndServe(":9091", nil)

实际上调用的是内部的 DefaultServeMux,这个 handler,如果我们想要实现自定义的路由处理器,则需要构建一个自定义的、实现了 Handler 接口的类实例作为 http.ListenAndServe 的第二个参数传入。

在开始介绍自定义路由处理器实现之前,我们先来看看 DefaultServeMux 是如何保存路由映射规则以及分发请求做路由匹配的。

顾名思义,DefaultServeMux 是 ServeMux 的默认实例:

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

这里的后缀 Mux 是 Multiplexer 的缩写,ServeMux 可以看作是 HTTP 请求的多路复用器,它们要实现的功能是:接受 HTTP 请求,然后基于映射规则将其转发给正确的处理器进行处理。

那么在 Go Web 应用中,这些路由映射规则是怎么定义的呢?

首先我们来看一下 ServeMux 的数据结构:

type ServeMux struct {
mu sync.RWMutex // 由于请求涉及到并发处理,因此这里需要一个锁机制
m map[string]muxEntry // 路由规则字典,存放 URL 路径与处理器的映射关系
es []muxEntry // MuxEntry 切片(按照最长到最短排序)
hosts bool // 路由规则中是否包含 host 信息
}

这里,我们需要重点关注的是 muxEntry 结构:

type muxEntry struct {
h Handler // 处理器具体实现
pattern string // 模式匹配字符串
}

最后我们来看一下 Handler 的定义,这是一个接口:

type Handler interface {
ServeHTTP(ResponseWriter, *Request) // 路由处理实现方法
}

当请求路径与 pattern 匹配时,就会调用 Handler 的 ServeHTTP 方法来处理请求。

以我们之前编写的示例应用为例,就是将 URL 路径为 / 的请求转发到 sayHelloWorld 进行处理:

http.HandleFunc("/", sayHelloWorld)

不过 sayHelloWorld 只是一个函数,并没有实现 Handler 接口,之所以可以成功添加到路由映射规则,是因为在底层通过 HandlerFunc() 函数将其强制转化为了 HandlerFunc 类型,而 HandlerFunc 类型实现了 ServeHTTP 方法,这样,sayHelloWorld 方法也就变相实现了 Handler 接口:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
// 这里实际上是将其路由映射规则保存到 DefaultServeMux 路由处理器的数据结构中(具体看下面)
mux.Handle(pattern, HandlerFunc(handler))
}

...

type HandlerFunc func(ResponseWriter, *Request)

// HandlerFunc 结构的方法
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

对于 sayHelloWorld 方法来说,它已然变成了 HandlerFunc 类型的函数类型,当我们在其实例上调用 ServeHTTP 方法时,调用的是 sayHelloWorld 方法本身。

前面我们提到,DefaultServeMux 是 ServeMux 的默认实例,当我们在 HandleFunc 中调用 mux.Handle 方法时,实际上是将其路由映射规则保存到 DefaultServeMux 路由处理器的数据结构中:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()

if pattern == "" {
panic("http: invalid pattern")
}
if handler == nil {
panic("http: nil handler")
}
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}

if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}

if pattern[0] != '/' {
mux.hosts = true
}
}

保存好路由映射规则之后,客户端请求又是怎么分发的呢?或者说请求 URL 与 DefaultServeMux 中保存的路由映射规则是如何匹配的呢?

上面说了处理客户端请求时,会调用默认 ServeMux 实现的 ServeHTTP 方法:

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
w.Header().Set("Connection", "close")
w.WriteHeader(StatusBadRequest)
return
}

h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}

如上所示,路由处理器接收到请求之后,如果 URL 路径是 *,则关闭连接,否则调用 mux.Handler(r) 返回对应请求路径匹配的处理器,然后执行 h.ServeHTTP(w, r),也就是调用对应路由 handler 的 ServerHTTP 方法,以 / 路由为例,调用的就是 sayHelloWorld 函数本身。

通过上面的介绍,我们了解了基于 DefaultServeMux 实现的整个路由规则存储(Web 应用启动期间进行)和请求匹配过程(客户端发起请求时进行),下面我们来看一下如何实现自定义的 路由处理器。

补充:自定义路由处理器

如果你搞清楚了上面的默认实现,编写自定义的路由处理器就会非常简单,我们只需要定义一个实现了 Handler 接口的类,然后将其实例传递给 http.ListenAndServe 方法即可:

package main

import (
"fmt"
"net/http"
)

type MyHander struct {

}

func (handler *MyHander) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
sayHelloGolang(w, r)
return
}
http.NotFound(w, r)
return
}

func sayHelloGolang(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello Golang!")
}

func main() {
handler := MyHander{}
http.ListenAndServe(":9091", &handler)
}

然后在浏览器中就可以访问 / 路由了:

这个实现很简单,而且我们并没有在应用启动期间初始化路由映射规则,而是在应用启动之后根据请求参数动态判断来做分发的,这样做会影响性能,而且非常不灵活,我们可以通过定义多个处理器的方式来解决这个问题:

package main

import (
"fmt"
"net/http"
)

type HelloHander struct {

}

func (handler *HelloHander) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sayHelloGolang(w, r)
}

func sayHelloGolang(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello Golang!")
}

type WorldHander struct {

}

func (handler *WorldHander) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}

func main() {
hello := HelloHander{}
world := WorldHander{}

server := http.Server{
Addr: ":9091",
}

http.Handle("/hello", &hello)
http.Handle("/world", &world)
server.ListenAndServe()
}

只是,我们又回到了老路子上,这里没有显式传入 handler,所以底层依然使用的是 DefaultServeMux 那套路由映射与请求分发机制,要实现完全自定义的、功能更加强大的处理器,只能通过自定义 ServeMux 来实现了

整个过程快速整理

如下代码

package main

import (
"net/http"
)

func SayHello(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello"))
}

func main() {
http.HandleFunc("/hello", SayHello)
http.ListenAndServe(":8001", nil)

}

Note:之所以这里 Response、Request 都是指针是因为在应用代码中需要设置响应头和响应实体,所以响应对象理应是指针类型。(这里 ResponseWriter 底层还是指针,下面会讲)

首先调用 Http.HandleFunc 按顺序做了几件事:

  • 调用了 DefaultServerMux 的 HandleFunc
  • 调用了 DefaultServerMux 的 Handle
  • 往 DefaultServeMux 的 map[string]muxEntry 中增加对应的 handler 和路由规则

其次调用 http.ListenAndServe(":8001", nil)

按顺序做了几件事情:

  • 实例化 Server
  • 调用 Server 的 ListenAndServe()
  • 调用 net.Listen("tcp", addr) 监听端口
  • 启动一个 for 循环,在循环体中 Accept 请求
  • 对每个请求实例化一个 Conn,并且开启一个 goroutine 为这个请求进行服务 go c.serve()
  • 读取每个请求的内容 w, err := c.readRequest()
  • 判断 header 是否为空,如果没有设置 handler(这个例子就没有设置 handler),handler 就设置为 DefaultServeMux
  • 调用 handler 的 ServeHttp
  • 在这个例子中,下面就进入到 DefaultServerMux.ServeHttp
  • 根据 request 选择 handler,并且进入到这个 handler 的 ServeHTTP
mux.handler(r).ServeHTTP(w, r)

选择 handler 的过程:

A 判断是否有路由能满足这个request(循环遍历ServerMux的muxEntry)
B 如果有路由满足,调用这个路由handler的ServeHttp
C 如果没有路由满足,调用NotFoundHandler的ServeHttp

net/http 和 gin 的关系

gin 更像是一个功能强大的路由器,提供更便捷的 web 服务解决方案,而其余功能则复用 net/http。 网络层实现,http parser 都是由 net/http 实现的。

两者的关系见下图所示

先来看下 net/http 的缺点

  • 请求响应编解码繁琐
  • 默认的 mutex 性能问题
  • 时间复杂度: O(n)O(n) + 正则匹配
  • 没有中间件、监控支持
  • 不太好的内存管理
  • request/response 无法复用(请求级别)
  • 无条件的解析请求头

gin 对这些点的改进

  • 实现了 http.Handler 接口的轻量级框架
  • 提供了高性能的路由:Radix Tree 实现(前缀树)
  • 提供工具简化了输入输出处理:binding 处理
  • 提供了中间件的支持
  • 提供 web 服务的常用工具函数,如 panic 捕获,json 格式校验等
  • 使用 context 池,减少 runtime 的 GC 工作量。
  • 强大的工具包: gin.Context

gin.Context 提供了一系列解析、校验请求的方法,其中内置了 validator 参数校验

Reference